/*
 * Copyright (c) 2013, Slick2D
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * - Neither the name of the Slick2D nor the names of its contributors may be
 *   used to endorse or promote products derived from this software without
 *   specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package org.newdawn.slick.openal;

import javazoom.jl.decoder.*;
import org.newdawn.slick.util.Log;

import java.io.IOException;
import java.io.InputStream;

/**
 * An input stream that can extract MP3 data.
 *
 * @author fluddokt (https://github.com/fluddokt)
 */
public class Mp3InputStream extends InputStream implements AudioInputStream {
    /**
     * The MPEG audio bitstream.
     */
    private Bitstream bitstream;

    /**
     * The MPEG decoder.
     */
    private Decoder decoder;

    /**
     * The frame header extractor.
     */
    private Header header;

    /**
     * The buffer.
     */
    private SampleBuffer buf;

    /**
     * The number of channels.
     */
    private int channels;

    /**
     * The sample rate.
     */
    private int sampleRate;

    /**
     * The buffer length.
     */
    private int bufLen = 0;

    /**
     * True if we've reached the end of the available data.
     */
    private boolean endOfStream = false;

    /**
     * The byte position.
     */
    private int bpos;

    /**
     * Create a new stream to decode MP3 data.
     *
     * @param input the input stream from which to read the MP3 file
     * @throws IOException failure to read the header from the input stream
     */
    public Mp3InputStream(InputStream input) throws IOException {
        decoder = new Decoder();
        bitstream = new Bitstream(input);
        try {
            header = bitstream.readFrame();
        } catch (BitstreamException e) {
            Log.error(e);
        }
        if (header == null) {
            close();
            throw new IOException("Failed to read header from MP3 input stream.");
        }

        channels = (header.mode() == Header.SINGLE_CHANNEL) ? 1 : 2;
        sampleRate = header.frequency();

        buf = new SampleBuffer(sampleRate, channels);
        decoder.setOutputBuffer(buf);

        try {
            decoder.decodeFrame(header, bitstream);
        } catch (DecoderException e) {
            Log.error(e);
        }

        bufLen = buf.getBufferLength();
        bitstream.closeFrame();
    }

    @Override
    public int read() throws IOException {
        if (atEnd())
            return -1;
        while (bpos / 2 >= bufLen) {
            try {
                header = bitstream.readFrame();
                if (header == null) {
                    buf.clear_buffer();

                    endOfStream = true;
                    return -1;
                }
                buf.clear_buffer();
                decoder.decodeFrame(header, bitstream);
                bufLen = buf.getBufferLength();
                bitstream.closeFrame();
            } catch (DecoderException | BitstreamException e) {
                Log.error(e);
            }
            bpos = 0;
        }
        int npos = bpos / 2;
        bpos++;

        if (bpos % 2 == 0)
            return (buf.getBuffer()[npos] >> 8) & 0xff;
        else
            return (buf.getBuffer()[npos]) & 0xff;
    }

    @Override
    public boolean atEnd() {
        return endOfStream;
    }

    @Override
    public int getChannels() {
        return channels;
    }

    @Override
    public int getRate() {
        return sampleRate;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        for (int i = 0; i < len; i++) {
            try {
                int value = read();
                if (value >= 0)
                    b[i] = (byte) value;
                else
                    return (i == 0) ? -1 : i;
            } catch (IOException e) {
                Log.error(e);
                return i;
            }
        }

        return len;
    }

    @Override
    public int read(byte[] b) throws IOException {
        return read(b, 0, b.length);
    }

    @Override
    public long skip(long length) {
        if (bufLen <= 0)
            Log.warn("Mp3InputStream: skip: bufLen not yet determined.");

        int skipped = 0;
        while (skipped + bufLen * 2 < length)
            try {
                header = bitstream.readFrame();
                if (header == null) {
                    endOfStream = true;
                    return -1;
                }

                // last frame that won't be skipped so better read it
                if (skipped + bufLen * 2 * 4 >= length || bufLen <= 0) {
                    buf.clear_buffer();
                    decoder.decodeFrame(header, bitstream);
                    bufLen = buf.getBufferLength();
                }
                skipped += bufLen * 2 - bpos;

                bitstream.closeFrame();
                bpos = 0;
            } catch (BitstreamException | DecoderException e) {
                Log.error(e);
            }
        if (bufLen * 2 - bpos > length - skipped) {
            bpos += length - skipped;
            skipped += length - skipped;
        }

        return skipped;
    }

    @Override
    public void close() throws IOException {
        try {
            bitstream.close();
        } catch (BitstreamException e) {
            e.printStackTrace();
        }
    }
}
